package controller;

import file.BadFileTypeException;
import file.TransferFile;
import file.TransferFileObserver;
import file.manager.FileManagerStartException;
import file.manager.TransferFileManager;

import java.io.File;
import java.net.InetAddress;
import java.net.InterfaceAddress;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.EnumMap;
import java.util.Set;

import network.AbstractNetworkInterface;
import network.ListenerBindException;
import network.SendSignalException;
import network.config.BadPortException;
import network.config.NetworkConfigObserver;
import network.config.NetworkConfiguration;
import network.config.UnsupportedProtocol;
import network.config.NetworkConfiguration.PortType;
import network.config.NetworkConfiguration.Protocol;
import network.tpa1.NetworkInterfaceA1;
import network.tpa2.NetworkInterfaceA2;
import network.tpb.NetworkInterfaceB;
import network.tprt2.NetworkInterfaceRT2;

import ui.MessageObserver;
import ui.StateObserver;
import user.LocalUser;
import user.User;
import user.UsersList;
import user.UsersListObserver;

public class MessageController {
	
	private UsersList users;
	/* Some observers... */
	private ArrayList <MessageObserver> messageObservers = null;
	private ArrayList <StateObserver> stateObservers = null;
	private ArrayList <TransferFileObserver> transferFileObservers = null ;
	private ArrayList <UsersListObserver> usersListObserver = null ;
	private AbstractNetworkInterface networkInterface = null;
	private TransferFileManager fileManager = null ;
	private WaitingMessageManager waitingMessageManager = null ;
	private NetworkConfiguration networkConfig ;
	
	public MessageController () {
		this.stateObservers = new ArrayList <StateObserver> () ;
		this.messageObservers = new ArrayList <MessageObserver> () ;
		this.transferFileObservers = new ArrayList <TransferFileObserver> () ;
		this.usersListObserver = new ArrayList <UsersListObserver> () ;
		this.networkConfig = new NetworkConfiguration() ;
	}
	
	/**
	 * Set the current allowed network interfaces.
	 * 
	 * @param addresses The new allowed interfaces
	 */
	public void setAllowedInterfaces (Set<InterfaceAddress> addresses) {
		for (InterfaceAddress ia : AbstractNetworkInterface.getAllLocalInterfaces()) {
			if (addresses.contains(ia)) {
				this.networkConfig.addAllowedInterface(ia) ;
			}
			else {
				this.networkConfig.removeAllowedInterface(ia) ;
			}
		}
	}
	
	/**
	 * Get the default allowed network interfaces.
	 * 
	 */
	public Set<InterfaceAddress> getDefaultAllowedInterfaces () {
		return this.networkConfig.getDefaultAllowedInterfaces () ;
	}
	
	/**
	 * Set protocol.
	 * 
	 * @param protocol The new protocol
	 * @throws UserAlreadyConnected 
	 */
	public void setProtocol (Protocol protocol) throws UserAlreadyConnected {
		if (connected()) {
			throw new UserAlreadyConnected() ;
		}
		this.networkConfig.setCurrentProtocol(protocol) ;
	}
	
	/**
	 * Set port for the specified protocol
	 * 
	 * @param protocol The protocol
	 * @param ports All the new ports for the specified protocol
	 * 
	 * @throws UserAlreadyConnected 
	 * @throws BadPortException 
	 */
	public void setPorts(Protocol protocol, EnumMap <PortType, Integer> ports) throws UserAlreadyConnected, BadPortException {
		if (connected()) {
			throw new UserAlreadyConnected() ;
		}
		EnumMap <PortType, Integer> savePorts = new EnumMap <PortType, Integer> (PortType.class) ;
		for (PortType p : PortType.values()) {
			savePorts.put(p, this.networkConfig.getPort(protocol, p)) ;
		}
		try {
			for (PortType p : PortType.values()) {
				this.networkConfig.setPort(protocol, p, ports.get(p)) ;
			}
		} 
		catch (BadPortException e) {
			for (PortType p : PortType.values()) {
				try {
					this.networkConfig.setPort(protocol, p, savePorts.get(p)) ;
				} 
				catch (BadPortException e1) {	}
			}
			throw new BadPortException (e) ;
		}
	}
	
	/**
	 * Set port for the current protocol
	 * 
	 * @param ports All the new ports for the specified protocol
	 * 
	 * @throws UserAlreadyConnected 
	 * @throws BadPortException 
	 */
	public void setPorts(EnumMap <PortType, Integer> ports) throws UserAlreadyConnected, BadPortException {
		setPorts(this.networkConfig.getCurrentProtocol(), ports) ;
	}
	
	/**
	 * Get the specified port for the current protocol
	 * 
	 * @param port The type of the port to return
	 * @return The port
	 */
	public int getPort (PortType port) {
		return this.networkConfig.getPort(port) ;
	}
	
	/**
	 * Get the current protocol
	 * 
	 * @return The current protocol
	 */
	public Protocol getCurrentProtocol () {
		return this.networkConfig.getCurrentProtocol() ;
	}
	
	/**
	 * Get the default protocol
	 * 
	 * @return The default protocol
	 */
	public Protocol getDefaultProtocol () {
		return this.networkConfig.getDefaultProtocol() ;
	}
	
	/**
	 * Get the specified default port for the specified protocol
	 * 
	 * @param p The protocol
	 * @param port The type of the port to return
	 * @return The port
	 */
	public int getDefaultPort (Protocol p, PortType port) {
		return this.networkConfig.getDefaultPort(port) ;
	}
	
	public MessageObserver addMessageObserver (MessageObserver mo) {
		this.messageObservers.add(mo) ;
		return mo ;
	}
	
	public MessageObserver removeMessageObserver (MessageObserver mo) {
		this.messageObservers.remove(mo) ;
		return mo ;
	}
	
	public StateObserver addStateObserver (StateObserver so) {
		this.stateObservers.add(so) ;
		return so ;
	}
	
	public StateObserver removeStateObserver (StateObserver so) {
		this.stateObservers.remove(so) ;
		return so ;
	}
	
	public NetworkConfigObserver addNetworkConfigObserver (NetworkConfigObserver nco) {
		this.networkConfig.addObserver(nco) ;
		return nco ;
	}
	
	public NetworkConfigObserver removeNetworkConfigObserver (NetworkConfigObserver nco) {
		this.networkConfig.removeObserver(nco) ;
		return nco ;
	}
	
	public TransferFileObserver addTransferFileObserver (TransferFileObserver tfo) {
		this.transferFileObservers.add(tfo) ;
		if (this.fileManager != null) {
			this.fileManager.addTransferFileObserver (tfo) ;
		}
		return tfo  ;
	}
	
	public TransferFileObserver removeTransferFileObserver (TransferFileObserver tfo) {
		this.transferFileObservers.remove(tfo) ;
		if (this.fileManager != null) {
			this.fileManager.removeTransferFileObserver (tfo) ;
		}
		return tfo  ;
	}
	
	public UsersListObserver addUsersListObserver (UsersListObserver ulo) {
		this.usersListObserver.add(ulo) ;
		if (this.users != null) {
			this.users.addObserver(ulo) ;
		}
		return ulo ;
	}
	
	public UsersListObserver removeUsersListObserver (UsersListObserver ulo) {
		this.usersListObserver.remove(ulo) ;
		if (this.users != null) {
			this.users.removeObserver(ulo) ;
		}
		return ulo ;
	}
	
	/**
	 * Check if the local user is connected (i.e, a Hello message has been sent and information about local user
	 * have been stored).
	 * 
	 * @return true if the user is connected
	 */
	public boolean connected () {
		return this.users != null && this.users.isConnected(LocalUser.getLocalUser().getIdentifier()) ;
	}
	
	/**
	 * Try to connect the local user with the specified username.
	 * 
	 * @param username The local user username
	 * @param messageObserver
	 * @return A NetworkInterface to deals with others user, the network interface is started before the function return.
	 * @throws SendSignalException if an error occurs sending the Hello signal
	 * @throws ListenerBindException if an error occurs starting the network interface
	 * @throws UserAlreadyConnected if the user is already connected
	 * @throws FileManagerStartException  if an error occurs starting the file manager
	 * @throws UnsupportedProtocol 
	 */
	public void performConnect (String username) throws SendSignalException, ListenerBindException, UserAlreadyConnected, FileManagerStartException, UnsupportedProtocol{
		if (connected()) {
			throw new UserAlreadyConnected() ;
		}
		/* Create users list and add observers... */
		this.users = new UsersList () ;
		for (UsersListObserver ulo : this.usersListObserver) {
			this.users.addObserver(ulo) ;
		}
		/* Update local user and fire state... */
		LocalUser.setLocalUserName(username);
		/* Create NetworkInterface... */
		switch (this.networkConfig.getCurrentProtocol()) {
		case A1:
			this.networkInterface = new NetworkInterfaceA1 (this, this.networkConfig) ;
			break ;
		case A2:
			this.networkInterface = new NetworkInterfaceA2(this, this.networkConfig) ;
			break ;
		case B:
			this.networkInterface = new NetworkInterfaceB(this, this.networkConfig) ;
			break ;
		case RT2:
			this.networkInterface = new NetworkInterfaceRT2(this, this.networkConfig) ;
			break ;
		default:
			throw new UnsupportedProtocol (this.networkConfig.getCurrentProtocol()) ;
		}
		this.networkInterface.start() ;
		/* Create file manager and add observer... */
		this.fileManager = new TransferFileManager(this.networkInterface, this.networkConfig) ;
		for (TransferFileObserver tfo : this.transferFileObservers) {
			fileManager.addTransferFileObserver(tfo) ;
		}
		this.fileManager.start () ;
		this.waitingMessageManager = new WaitingMessageManager () ;
		this.users.addOrUpdate(LocalUser.getLocalUser()) ;
		for (StateObserver so : this.stateObservers) {
			so.fireConnect() ;
		}
		this.networkInterface.sendHello(username);
	}
	
	/**
	 * Change the local user name (send a new Hello signal on broadcast with new username)
	 * 
	 * @param newUsername
	 * @throws UserNotConnected
	 * @throws SendSignalException
	 */
	public void changeUsername (String newUsername) throws UserNotConnected, SendSignalException {
		if (!connected()) {
			throw new UserNotConnected() ;
		}
		LocalUser.setLocalUserName(newUsername) ;
		this.users.addOrUpdate(LocalUser.getLocalUser()) ;
		this.networkInterface.sendHello(newUsername) ;
	}
	
	/**
	 * Process a Hello signal, i.e add user to remote users list.
	 * 
	 * @param username The remote user username
	 * @param address The remote user address
	 * @throws SendSignalException if an error occurs sending HelloReply signal
	 * @throws UserNotConnected if the local user is not connected
	 */
	public void processHello (String username, InetAddress address) throws SendSignalException, UserNotConnected {
		if (!connected()) {
			throw new UserNotConnected() ;
		}
		User remoteUser = new User (username, address) ;
		this.users.addOrUpdate(remoteUser);
		this.networkInterface.sendHelloReply(LocalUser.getLocalUser().getUsername(), address);
	}
	
	/**
	 * Process a HelloReply signal, i.e add user to remote users list.
	 * 
	 * @param username The remote user username
	 * @param address The remote user address
	 * @throws UserNotConnected if the local user is not connected
	 */
	public void processHelloReply (String username, InetAddress address) throws UserNotConnected {
		if (!connected()) {
			throw new UserNotConnected() ;
		}
		User remoteUser = new User (username, address) ;
		this.users.addOrUpdate(remoteUser);
		/* If there is some message waiting from the host... */
		if (this.waitingMessageManager.contains(address.getHostAddress())) {
			/* Get all the message, and try to process it. */
			ArrayList <String> msgs = this.waitingMessageManager.getMessages(address.getHostAddress()) ;
			ArrayList <InetAddress[]> addrs = this.waitingMessageManager.getReceivers(address.getHostAddress()) ;
			for (int i=0; i<msgs.size(); i++) {
				try {
					processSendText(msgs.get(i), addrs.get(i), address) ;
				}
				catch (SendSignalException e) {

				}
			}
			this.waitingMessageManager.remove(address.getHostAddress()) ;
		}
	}
	
	/**
	 * Disconnect the local user and send a GoodBye signal on broadcast
	 * 
	 * @throws SendSignalException if an error occurs sending GoodBye signal
	 * @throws UserNotConnected if the user is not connected
	 */
	public void performDisconnect () throws SendSignalException, UserNotConnected {
		if (!connected()) {
			throw new UserNotConnected() ;
		}
		this.users.removeAll() ;
		this.networkInterface.sendGoodBye();
		this.networkInterface.finish() ;
		synchronized (this.networkInterface) {
			this.networkInterface.notify() ;
		}
		this.fileManager.finish() ;
		synchronized (this.fileManager) {
			this.fileManager.notify() ;
		}
		for (StateObserver so : this.stateObservers) {
			so.fireDisconnect() ;
		}
	}
	
	/**
	 * Process GoodBye signal, i.e remove user from remote users list
	 * 
	 * @param address The remote user address
	 * @throws UserNotConnected if the local user is not connected
	 */
	public void processGoodBye (InetAddress address) throws UserNotConnected {
		if (!connected()) {
			throw new UserNotConnected() ;
		}
		this.users.delete (User.createIdentifier(address));
	}

	/**
	 * Process AskLogin signal, i.e send a HelloReply signal to the specified user
	 * 
	 * @param addr The remote user address
	 * @throws SendSignalException if an error occurs sending the signal
	 * @throws UserNotConnected if the local user is not connected
	 */
	public void processAskLogin(InetAddress addr) throws SendSignalException, UserNotConnected {
		if (!connected()) {
			throw new UserNotConnected() ;
		}
		this.networkInterface.sendHelloReply(LocalUser.getLocalUser().getUsername(), addr) ;		
	}
	
	/**
	 * Send a message to the specified user
	 * 
	 * @param users Users that will receive the message, if null, the message is send to all users connected
	 * @param message The message that will be sent
	 * @throws SendSignalException if an error occurs sending the signal
	 * @throws UserNotConnected if the local user is not connected
	 */
	public void sendText (Set <User> users, String message) throws UserNotConnected, SendSignalException {
		if (!connected()) {
			throw new UserNotConnected() ;
		}
		if (users == null || users.size() == 0) {
			/* Broadcast... */
			for (MessageObserver mo : this.messageObservers) {
				mo.fireNewMessage(LocalUser.getLocalUser(), null, message) ;
			}
			this.networkInterface.sendMessage(message, null, null) ;
		}
		else  {
			for (MessageObserver mo : this.messageObservers) {
				mo.fireNewMessage(LocalUser.getLocalUser(), new ArrayList <User> (users), message) ;
			}
			/* Get all the receivers address, and send the signal to all of them. */
			ArrayList <InetAddress> receivers = new ArrayList <InetAddress> () ;
			for (User u : users) {
				receivers.add(u.getAddress()) ;
			}
			InetAddress[] tmp = new InetAddress[receivers.size()] ;
			for (User u  : users) {
				this.networkInterface.sendMessage(message, u.getAddress(), receivers.toArray(tmp)) ;
			}
		}
	}
	
	/**
	 * Process SendText signal, i.e display message to the user (according to the specified gui).
	 * Send a AskLogin if the sender is not stored in the list.
	 * 
	 * @param message
	 * @param receivers
	 * @param addr
	 * @throws UserNotConnected if the local user is not connected
	 * @throws SendSignalException if an error occurs sending the AskLogin signal
	 */
	public void processSendText(String message, InetAddress[] receivers, InetAddress address) throws UserNotConnected, SendSignalException {
		if (!connected()) {
			throw new UserNotConnected() ;
		}
		User u = this.users.get(User.createIdentifier(address)) ;
		if (u == null) {
			/* The sender is unknow, so we send a AskLogin. */
			this.waitingMessageManager.add(address.getHostAddress(), message, receivers) ;
			this.networkInterface.sendAskLogin(address) ;
		}
		else if (receivers == null || receivers.length == 0){
			/* There is no receivers specified (broadcast). */
			for (MessageObserver mo : this.messageObservers) {
				mo.fireNewMessage (u, null, message) ;
			}
		}
		else {
			/* See if there is a receiver we don't know...
			 * If there is one, we stop the loop and send a ask 
			 * login to him. */
			User[] userReceivers = new User[receivers.length] ;
			boolean allOk = true ;
			for (int i=0; i<receivers.length && allOk; ++i) {
				InetAddress addr = receivers[i] ;
				userReceivers[i] = this.users.get(User.createIdentifier(addr)) ;
				if (!this.networkConfig.isAllowedAddress(addr)) {
					continue ;
				}
				if (receivers[i] == null) {
					this.waitingMessageManager.add(address.getHostAddress(), message, receivers) ;
					this.networkInterface.sendAskLogin(addr) ;
					allOk = false ;
				}
			}
			if (allOk) {
				for (MessageObserver mo : this.messageObservers) {
					mo.fireNewMessage (u, new ArrayList <User> (Arrays.asList(userReceivers)), message) ;
				}
			}
		}
	}
	
	/**
	 * Process a AcceptFile signal, i.e send or remove the file waiting in the file queue
	 * 
	 * @param fileID The file ID
	 * @param accept true if the file has been accepted
	 * @param now true if the file can be send now
	 * @param addr The address of the host that want to receive its file
	 */
	public void processAcceptFile (int fileID, boolean accepted, boolean now, InetAddress addr) {
		if (!this.users.isConnected(User.createIdentifier(addr))) {
			return ;
		}
		if (!accepted) {
			TransferFile tf = this.fileManager.removeFileToSend(fileID) ;
			if (tf != null) {
				for (MessageObserver mo : this.messageObservers) {
					mo.fireFileResponse(tf, false) ;
				}
			}
		}
		else if (now) {
			TransferFile tf = this.fileManager.sendFile(fileID) ;
			if (tf != null) {
				for (MessageObserver mo : this.messageObservers) {
					mo.fireFileResponse(tf, true) ;
				}
			}
		}
	}
	
	/**
	 * Add the file specified to fileManager send file queue and ask the remote user if he want to
	 * receive the file
	 * 
	 * @param file The file to add to the queue
	 * @param to The user that will receive the file
	 * @return The ID of the file
	 * @throws BadFileTypeException if there is a problem with the file (does not exist, is null, is not readable)
	 * @throws SendSignalException if an error occurs sending the PropFile signal
	 */
	public TransferFile sendFile (File file, User to) throws BadFileTypeException, SendSignalException {
		TransferFile tf = this.fileManager.addFileToSend(file, to) ;
		this.networkInterface.sendPropFile(tf.getFileName(), tf.getSize(), tf.getID(), to.getAddress()) ;
		for (MessageObserver mo : this.messageObservers) {
			mo.fireLocalFileQuery(tf) ;
		}
		return tf ;
	}
	
	/**
	 * Send a AcceptFile signal corresponding to specified TransferFile accepted
	 * 
	 * @param tf The new TransferFile
	 * @throws SendSignalException if an error occurs sending the signal
	 */
	public void sendFileAccepted (TransferFile tf) throws SendSignalException {
		this.fileManager.acceptFile(tf) ;
		synchronized (this.fileManager) {
			this.fileManager.notify() ;
		}
	}
	
	/**
	 * Send a AcceptFile signal corresponding to specified TransferFile refused
	 * 
	 * @param sender
	 * @param fileID
	 * @throws SendSignalException if an error occurs sending the signal
	 */
	public void sendFileRefused (TransferFile tf) throws SendSignalException {
		this.fileManager.refuseFile(tf) ;
		this.networkInterface.sendAcceptFile (tf.getSender().getAddress(), tf.getID(), false, false) ;
		for (MessageObserver mo : this.messageObservers) {
			mo.fireFileResponse(tf, false) ;
		}
	}

	/**
	 * Process a PropFile signal, display the user some informations about the he could receive
	 * 
	 * @param filename
	 * @param fileID
	 * @param fileSize
	 * @param addr The address of the sender
	 */
	public void processPropFile(String filename, int fileID, long fileSize, InetAddress addr) throws SendSignalException {
		if (!this.users.isConnected(User.createIdentifier(addr))) {
			return ;
		}
		TransferFile tf = new TransferFile(filename, fileID, fileSize, this.users.get(User.createIdentifier(addr))) ;
		for (MessageObserver mo : this.messageObservers) {
			mo.fireRemoteFileQuery (tf);
		}
		this.fileManager.addFileToAccept(tf);
	}
	
	/**
	 * Get a waiting to accept file into the queue
	 * 
	 * @param identifier The identifier of the file
	 */
	public TransferFile getWaitingToAcceptFile (String identifier) {
		return this.fileManager.getWaitingToAcceptFile(identifier) ;
	}
	
	/**
	 * Get a waiting to send file into the queue
	 * 
	 * @param identifier The identifier of the file
	 */
	public TransferFile getWaitingToSendFile (String identifier) {
		return this.fileManager.getWaitingToSendFile(identifier) ;
	}

}
